/*
 * Minecraft Forge
 * Copyright (c) 2016.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation version 2.1
 * of the License.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package net.minecraftforge.common.animation;

import java.io.IOException;
import java.util.regex.Pattern;

import net.minecraft.util.IStringSerializable;

import java.util.function.Function;

import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import javax.annotation.Nullable;

/**
 * Various implementations of ITimeValue.
 */
public final class TimeValues {
    public static enum IdentityValue implements ITimeValue, IStringSerializable {
        INSTANCE;

        @Override
        public float apply(float input) {
            return input;
        }

        @Override
        public String getName() {
            return "identity";
        }
    }

    public static final class ConstValue implements ITimeValue {
        private final float output;

        public ConstValue(float output) {
            this.output = output;
        }

        @Override
        public float apply(float input) {
            return output;
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(output);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            ConstValue other = (ConstValue) obj;
            return output == other.output;
        }
    }

    /**
     * Simple value holder.
     */
    public static final class VariableValue implements ITimeValue {
        private float output;

        public VariableValue(float initialValue) {
            this.output = initialValue;
        }

        public void setValue(float newValue) {
            this.output = newValue;
        }

        @Override
        public float apply(float input) {
            return output;
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(output);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            VariableValue other = (VariableValue) obj;
            return output == other.output;
        }
    }

    public static final class SimpleExprValue implements ITimeValue {
        private static final Pattern opsPattern = Pattern.compile("[+\\-*/mMrRfF]+");

        private final String operators;
        private final ImmutableList<ITimeValue> args;

        public SimpleExprValue(String operators, ImmutableList<ITimeValue> args) {
            this.operators = operators;
            this.args = args;
        }

        @Override
        public float apply(float input) {
            float ret = input;
            for (int i = 0; i < operators.length(); i++) {
                float arg = args.get(i).apply(input);
                switch (operators.charAt(i)) {
                    case '+':
                        ret += arg;
                        break;
                    case '-':
                        ret -= arg;
                        break;
                    case '*':
                        ret *= arg;
                        break;
                    case '/':
                        ret /= arg;
                        break;
                    case 'm':
                        ret = Math.min(ret, arg);
                        break;
                    case 'M':
                        ret = Math.max(ret, arg);
                        break;
                    case 'r':
                        ret = (float) Math.floor(ret / arg) * arg;
                        break;
                    case 'R':
                        ret = (float) Math.ceil(ret / arg) * arg;
                        break;
                    case 'f':
                        ret -= Math.floor(ret / arg) * arg;
                        break;
                    case 'F':
                        ret = (float) Math.ceil(ret / arg) * arg - ret;
                        break;
                }
            }
            return ret;
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(operators, args);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            SimpleExprValue other = (SimpleExprValue) obj;
            return Objects.equal(operators, other.operators) && Objects.equal(args, other.args);
        }
    }

    public static final class CompositionValue implements ITimeValue {
        private final ITimeValue g;
        private final ITimeValue f;

        public CompositionValue(ITimeValue g, ITimeValue f) {
            super();
            this.g = g;
            this.f = f;
        }

        @Override
        public float apply(float input) {
            return g.apply(f.apply(input));
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(g, f);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            CompositionValue other = (CompositionValue) obj;
            return Objects.equal(g, other.g) && Objects.equal(f, other.f);
        }
    }

    public static final class ParameterValue implements ITimeValue, IStringSerializable {
        private final String parameterName;
        private final Function<String, ITimeValue> valueResolver;
        private ITimeValue parameter;

        public ParameterValue(String parameterName, Function<String, ITimeValue> valueResolver) {
            this.parameterName = parameterName;
            this.valueResolver = valueResolver;
        }

        @Override
        public String getName() {
            return parameterName;
        }

        private void resolve() {
            if (parameter == null) {
                if (valueResolver != null) {
                    parameter = valueResolver.apply(parameterName);
                }
                if (parameter == null) {
                    throw new IllegalArgumentException("Couldn't resolve parameter value " + parameterName);
                }
            }
        }

        @Override
        public float apply(float input) {
            resolve();
            return parameter.apply(input);
        }

        @Override
        public int hashCode() {
            resolve();
            return parameter.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            ParameterValue other = (ParameterValue) obj;
            resolve();
            other.resolve();
            return Objects.equal(parameter, other.parameter);
        }
    }

    public static enum CommonTimeValueTypeAdapterFactory implements TypeAdapterFactory {
        INSTANCE;

        private final ThreadLocal<Function<String, ITimeValue>> valueResolver = new ThreadLocal<Function<String, ITimeValue>>();

        public void setValueResolver(@Nullable Function<String, ITimeValue> valueResolver) {
            this.valueResolver.set(valueResolver);
        }

        @Override
        @SuppressWarnings("unchecked")
        @Nullable
        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
            if (type.getRawType() != ITimeValue.class) {
                return null;
            }

            return (TypeAdapter<T>) new TypeAdapter<ITimeValue>() {
                @Override
                public void write(JsonWriter out, ITimeValue parameter) throws IOException {
                    if (parameter instanceof ConstValue) {
                        out.value(((ConstValue) parameter).output);
                    } else if (parameter instanceof SimpleExprValue) {
                        SimpleExprValue p = (SimpleExprValue) parameter;
                        out.beginArray();
                        out.value(p.operators);
                        for (ITimeValue v : p.args) {
                            write(out, v);
                        }
                        out.endArray();
                    } else if (parameter instanceof CompositionValue) {
                        CompositionValue p = (CompositionValue) parameter;
                        out.beginArray();
                        out.value("compose");
                        write(out, p.g);
                        write(out, p.f);
                        out.endArray();
                    } else if (parameter instanceof IStringSerializable) {
                        out.value("#" + ((IStringSerializable) parameter).getName());
                    }
                }

                @Override
                public ITimeValue read(JsonReader in) throws IOException {
                    switch (in.peek()) {
                        case NUMBER:
                            return new ConstValue((float) in.nextDouble());
                        case BEGIN_ARRAY:
                            in.beginArray();
                            String type = in.nextString();
                            ITimeValue p;
                            if (SimpleExprValue.opsPattern.matcher(type).matches()) {
                                ImmutableList.Builder<ITimeValue> builder = ImmutableList.builder();
                                while (in.hasNext()) {
                                    builder.add(read(in));
                                }
                                p = new SimpleExprValue(type, builder.build());
                            } else if ("compose".equals(type)) {
                                p = new CompositionValue(read(in), read(in));
                            } else {
                                throw new IOException("Unknown TimeValue type \"" + type + "\"");
                            }
                            in.endArray();
                            return p;
                        case STRING:
                            String string = in.nextString();
                            if (string.equals("#identity")) {
                                return IdentityValue.INSTANCE;
                            }
                            if (!string.startsWith("#")) {
                                throw new IOException("Expected TimeValue reference, got \"" + string + "\"");
                            }
                            // User Parameter TimeValue
                            return new ParameterValue(string.substring(1), valueResolver.get());
                        default:
                            throw new IOException("Expected TimeValue, got " + in.peek());
                    }
                }
            };
        }
    }
}
